sysrot-hub
Version:
CLI de nueva generación para proyectos Next.js 14+ con IA multi-modelo, Web3 integration, internacionalización completa y roadmap realista 2025-2026
590 lines (516 loc) • 17.5 kB
text/typescript
import { NextApiRequest, NextApiResponse } from 'next';
import { PrismaClient } from '@prisma/client';
import { getSession } from 'next-auth/react';
import { z } from 'zod';
import { v4 as uuidv4 } from 'uuid';
const prisma = new PrismaClient();
// Validation schemas
const startConversationSchema = z.object({
visitorId: z.string().optional(),
visitorInfo: z.record(z.any()).optional(),
source: z.string().optional(),
referrer: z.string().optional(),
userAgent: z.string().optional(),
ipAddress: z.string().optional(),
location: z.record(z.any()).optional(),
customFields: z.record(z.any()).optional()
});
const sendMessageSchema = z.object({
content: z.string().min(1, 'Message content is required').max(4000, 'Message too long'),
contentType: z.enum(['TEXT', 'IMAGE', 'VIDEO', 'AUDIO', 'FILE']).default('TEXT'),
metadata: z.record(z.any()).optional(),
visitorId: z.string().optional()
});
const rateConversationSchema = z.object({
rating: z.number().min(1).max(5),
feedback: z.string().max(1000).optional(),
categories: z.record(z.any()).optional(),
messageId: z.string().optional()
});
async function getChatbotAccess(chatbotId: string, userId?: string) {
const chatbot = await prisma.chatbot.findUnique({
where: { id: chatbotId },
include: {
organization: true
}
});
if (!chatbot || !chatbot.isActive) {
return { hasAccess: false, chatbot: null };
}
// Public chatbots are accessible to everyone
if (chatbot.isPublic) {
return { hasAccess: true, chatbot };
}
// Check if user has access to this chatbot
if (!userId) {
return { hasAccess: false, chatbot: null };
}
// Owner has access
if (chatbot.createdBy === userId) {
return { hasAccess: true, chatbot };
}
// Organization members have access
if (chatbot.organizationId) {
const membership = await prisma.organizationMember.findUnique({
where: {
organizationId_userId: {
organizationId: chatbot.organizationId,
userId
}
}
});
if (membership) {
return { hasAccess: true, chatbot };
}
}
return { hasAccess: false, chatbot: null };
}
// Simple intent matching function
async function matchIntent(chatbotId: string, userMessage: string) {
const intents = await prisma.botIntent.findMany({
where: {
chatbotId,
isEnabled: true
},
include: {
responses: {
where: { isEnabled: true },
orderBy: { weight: 'desc' }
}
}
});
const normalizedMessage = userMessage.toLowerCase().trim();
// Try to match with examples
for (const intent of intents) {
for (const example of intent.examples) {
if (normalizedMessage.includes(example.toLowerCase())) {
return {
intent: intent.name,
confidence: 0.8,
response: intent.responses[0]?.content || null
};
}
}
// Try to match with patterns (simple regex)
for (const pattern of intent.patterns) {
try {
const regex = new RegExp(pattern, 'i');
if (regex.test(normalizedMessage)) {
return {
intent: intent.name,
confidence: 0.9,
response: intent.responses[0]?.content || null
};
}
} catch (error) {
console.warn('Invalid regex pattern:', pattern);
}
}
}
// Fallback to unknown intent
const unknownIntent = intents.find(i => i.name === 'unknown');
return {
intent: 'unknown',
confidence: 0.1,
response: unknownIntent?.responses[0]?.content || "I'm not sure how to help with that."
};
}
// Generate AI response using existing AI API
async function generateAIResponse(chatbot: any, conversationHistory: any[], userMessage: string) {
try {
// Build conversation context
const contextMessages = conversationHistory.slice(-10).map(msg => ({
role: msg.role === 'USER' ? 'user' : 'assistant',
content: msg.content
}));
// Add current user message
contextMessages.push({
role: 'user',
content: userMessage
});
// Call the AI API
const response = await fetch(`${process.env.NEXTAUTH_URL}/api/ai`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
messages: contextMessages,
model: chatbot.model,
temperature: chatbot.temperature,
max_tokens: chatbot.maxTokens,
system: chatbot.systemPrompt
})
});
if (!response.ok) {
throw new Error('Failed to get AI response');
}
const data = await response.json();
return {
content: data.response || data.text || "I apologize, but I couldn't process your request right now.",
tokensUsed: data.usage?.total_tokens || 0,
model: chatbot.model
};
} catch (error) {
console.error('AI Response error:', error);
// Fallback to intent-based response
const intentMatch = await matchIntent(chatbot.id, userMessage);
return {
content: intentMatch.response || "I'm having trouble right now. Please try again later.",
tokensUsed: 0,
model: 'fallback',
intent: intentMatch.intent,
confidence: intentMatch.confidence
};
}
}
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { chatbotId, action } = req.query;
const session = await getSession({ req });
if (!chatbotId || typeof chatbotId !== 'string') {
return res.status(400).json({ error: 'Chatbot ID is required' });
}
// Check chatbot access
const { hasAccess, chatbot } = await getChatbotAccess(chatbotId, session?.user?.id);
if (!hasAccess || !chatbot) {
return res.status(403).json({ error: 'Access denied or chatbot not found' });
}
const { method } = req;
if (action === 'start' && method === 'POST') {
return await startConversation(req, res, chatbot);
} else if (action === 'message' && method === 'POST') {
return await sendMessage(req, res, chatbot);
} else if (action === 'rate' && method === 'POST') {
return await rateConversation(req, res);
}
switch (method) {
case 'GET':
return await getConversations(req, res, chatbot);
default:
res.setHeader('Allow', ['GET', 'POST']);
return res.status(405).end('Method Not Allowed');
}
} catch (error) {
console.error('Chatbot conversations API error:', error);
return res.status(500).json({ error: 'Internal server error' });
} finally {
await prisma.$disconnect();
}
}
async function getConversations(req: NextApiRequest, res: NextApiResponse, chatbot: any) {
const session = await getSession({ req });
const { limit = '20', page = '1', status, userId } = req.query;
try {
const pageNum = parseInt(page as string);
const limitNum = parseInt(limit as string);
const offset = (pageNum - 1) * limitNum;
const whereClause: any = {
chatbotId: chatbot.id
};
if (status && typeof status === 'string') {
whereClause.status = status.toUpperCase();
}
if (userId && typeof userId === 'string') {
whereClause.userId = userId;
}
const conversations = await prisma.botConversation.findMany({
where: whereClause,
include: {
user: {
select: {
id: true,
name: true,
email: true,
image: true
}
},
messages: {
orderBy: { createdAt: 'desc' },
take: 1,
include: {
conversation: false
}
},
ratings: {
orderBy: { createdAt: 'desc' },
take: 1
},
_count: {
select: {
messages: true
}
}
},
orderBy: {
lastMessageAt: 'desc'
},
skip: offset,
take: limitNum
});
const total = await prisma.botConversation.count({
where: whereClause
});
res.status(200).json({
conversations,
pagination: {
page: pageNum,
limit: limitNum,
total,
pages: Math.ceil(total / limitNum)
}
});
} catch (error) {
console.error('Error fetching conversations:', error);
res.status(500).json({ error: 'Failed to fetch conversations' });
}
}
async function startConversation(req: NextApiRequest, res: NextApiResponse, chatbot: any) {
const session = await getSession({ req });
try {
const validatedData = startConversationSchema.parse(req.body);
// Check conversation limits
if (chatbot.maxConversations) {
const activeConversations = await prisma.botConversation.count({
where: {
chatbotId: chatbot.id,
status: 'ACTIVE'
}
});
if (activeConversations >= chatbot.maxConversations) {
return res.status(429).json({
error: 'Maximum active conversations reached',
fallbackMessage: chatbot.fallbackMessage
});
}
}
// Create conversation
const conversation = await prisma.botConversation.create({
data: {
chatbotId: chatbot.id,
userId: session?.user?.id,
visitorId: validatedData.visitorId || uuidv4(),
visitorInfo: validatedData.visitorInfo,
source: validatedData.source,
referrer: validatedData.referrer,
userAgent: validatedData.userAgent,
ipAddress: validatedData.ipAddress,
location: validatedData.location,
customFields: validatedData.customFields,
sessionId: uuidv4()
},
include: {
chatbot: {
select: {
id: true,
name: true,
avatar: true,
fallbackMessage: true
}
}
}
});
// Send welcome message if configured
let welcomeMessage = null;
if (chatbot.systemPrompt.includes('welcome') || chatbot.fallbackMessage) {
const welcomeContent = chatbot.fallbackMessage ||
`Hello! I'm ${chatbot.name}. How can I help you today?`;
welcomeMessage = await prisma.botMessage.create({
data: {
conversationId: conversation.id,
role: 'ASSISTANT',
content: welcomeContent,
sender: chatbot.name,
contentType: 'TEXT'
}
});
}
res.status(201).json({
conversation: {
...conversation,
messages: welcomeMessage ? [welcomeMessage] : []
}
});
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
error: 'Validation failed',
details: error.errors
});
}
console.error('Error starting conversation:', error);
res.status(500).json({ error: 'Failed to start conversation' });
}
}
async function sendMessage(req: NextApiRequest, res: NextApiResponse, chatbot: any) {
const session = await getSession({ req });
const { conversationId } = req.query;
if (!conversationId || typeof conversationId !== 'string') {
return res.status(400).json({ error: 'Conversation ID is required' });
}
try {
const validatedData = sendMessageSchema.parse(req.body);
// Get conversation
const conversation = await prisma.botConversation.findUnique({
where: { id: conversationId },
include: {
messages: {
orderBy: { createdAt: 'desc' },
take: 10
}
}
});
if (!conversation) {
return res.status(404).json({ error: 'Conversation not found' });
}
// Check if user can send messages to this conversation
const canSendMessage = conversation.userId === session?.user?.id ||
conversation.visitorId === validatedData.visitorId ||
!session?.user; // Allow anonymous messages
if (!canSendMessage) {
return res.status(403).json({ error: 'Cannot send message to this conversation' });
}
const startTime = Date.now();
// Create user message
const userMessage = await prisma.botMessage.create({
data: {
conversationId: conversation.id,
role: 'USER',
content: validatedData.content,
contentType: validatedData.contentType,
metadata: validatedData.metadata,
sender: session?.user?.name || 'Anonymous'
}
});
// Generate AI response
const aiResponse = await generateAIResponse(chatbot, conversation.messages, validatedData.content);
const responseTime = Date.now() - startTime;
// Create assistant message
const assistantMessage = await prisma.botMessage.create({
data: {
conversationId: conversation.id,
role: 'ASSISTANT',
content: aiResponse.content,
contentType: 'TEXT',
sender: chatbot.name,
intent: aiResponse.intent,
confidence: aiResponse.confidence,
prompt: chatbot.systemPrompt,
modelUsed: aiResponse.model,
tokensUsed: aiResponse.tokensUsed,
responseTime
}
});
// Update conversation timestamp
await prisma.botConversation.update({
where: { id: conversation.id },
data: { lastMessageAt: new Date() }
});
// Update analytics
await updateAnalytics(chatbot.id, {
messagesReceived: 1,
messagesSent: 1,
averageResponseTime: responseTime,
tokensUsed: aiResponse.tokensUsed
});
res.status(201).json({
userMessage,
assistantMessage,
conversationId: conversation.id
});
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
error: 'Validation failed',
details: error.errors
});
}
console.error('Error sending message:', error);
res.status(500).json({ error: 'Failed to send message' });
}
}
async function rateConversation(req: NextApiRequest, res: NextApiResponse) {
const { conversationId } = req.query;
if (!conversationId || typeof conversationId !== 'string') {
return res.status(400).json({ error: 'Conversation ID is required' });
}
try {
const validatedData = rateConversationSchema.parse(req.body);
// Check if conversation exists
const conversation = await prisma.botConversation.findUnique({
where: { id: conversationId }
});
if (!conversation) {
return res.status(404).json({ error: 'Conversation not found' });
}
// Create rating
const rating = await prisma.botRating.create({
data: {
conversationId,
rating: validatedData.rating,
feedback: validatedData.feedback,
categories: validatedData.categories,
messageId: validatedData.messageId
}
});
// Update analytics
await updateAnalytics(conversation.chatbotId, {
ratingsReceived: 1,
averageRating: validatedData.rating,
positiveRatings: validatedData.rating >= 4 ? 1 : 0,
negativeRatings: validatedData.rating <= 2 ? 1 : 0
});
res.status(201).json({
rating,
message: 'Rating submitted successfully'
});
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
error: 'Validation failed',
details: error.errors
});
}
console.error('Error rating conversation:', error);
res.status(500).json({ error: 'Failed to submit rating' });
}
}
// Helper function to update analytics
async function updateAnalytics(chatbotId: string, metrics: any) {
const today = new Date();
today.setHours(0, 0, 0, 0);
try {
await prisma.botAnalytics.upsert({
where: {
chatbotId_date_period: {
chatbotId,
date: today,
period: 'DAY'
}
},
update: {
messagesReceived: { increment: metrics.messagesReceived || 0 },
messagesSent: { increment: metrics.messagesSent || 0 },
ratingsReceived: { increment: metrics.ratingsReceived || 0 },
positiveRatings: { increment: metrics.positiveRatings || 0 },
negativeRatings: { increment: metrics.negativeRatings || 0 },
totalTokensUsed: { increment: metrics.tokensUsed || 0 },
averageResponseTime: metrics.averageResponseTime || undefined,
averageRating: metrics.averageRating || undefined
},
create: {
chatbotId,
date: today,
period: 'DAY',
messagesReceived: metrics.messagesReceived || 0,
messagesSent: metrics.messagesSent || 0,
ratingsReceived: metrics.ratingsReceived || 0,
positiveRatings: metrics.positiveRatings || 0,
negativeRatings: metrics.negativeRatings || 0,
totalTokensUsed: metrics.tokensUsed || 0,
averageResponseTime: metrics.averageResponseTime || 0,
averageRating: metrics.averageRating || 0
}
});
} catch (error) {
console.error('Error updating analytics:', error);
}
}